﻿/* 
 * Copyright (c) Intel Corporation
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * -- Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * -- Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * -- Neither the name of the Intel Corporation nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE INTEL OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Net;
using System.IO;
using System.Text;
using System.Xml.Serialization;
using ExtensionLoader;
using OpenMetaverse;
using OpenMetaverse.StructuredData;
using CableBeachMessages;

using AssetType = OpenMetaverse.AssetType;
using InventoryType = OpenMetaverse.InventoryType;

namespace InventoryServer.Extensions
{
    public class SimpleFilesystem : IExtension<InventoryServer>, IFilesystemProvider
    {
        const string EXTENSION_NAME = "SimpleFilesystem"; // Used for metrics reporting
        const string DEFAULT_INVENTORY_DIR = InventoryServer.DATA_DIR + "SimpleFilesystem";

        InventoryServer server;
        Dictionary<UUID, InventoryCollection> inventories = new Dictionary<UUID, InventoryCollection>();
        Dictionary<UUID, List<InventoryItem>> activeGestures = new Dictionary<UUID, List<InventoryItem>>();
        XmlSerializer itemSerializer = new XmlSerializer(typeof(InventoryItem));
        XmlSerializer folderSerializer = new XmlSerializer(typeof(InventoryFolder));

        public SimpleFilesystem()
        {
        }

        #region Required Interfaces

        public bool Start(InventoryServer server)
        {
            this.server = server;

            LoadFiles();

            Logger.InfoFormat("[SimpleInventory] Initialized the inventory index with data for {0} avatars",
                inventories.Count);

            return true;
        }

        public void Stop()
        {
        }

        public BackendResponse TryFetchItem(Uri owner, UUID agentID, UUID itemID, out InventoryItem item)
        {
            item = null;
            UUID ownerID = CableBeachUtils.MessageToUUID(owner, agentID);
            BackendResponse ret;

            InventoryCollection collection;
            if (inventories.TryGetValue(ownerID, out collection) && collection.Items.TryGetValue(itemID, out item))
                ret = BackendResponse.Success;
            else
                ret = BackendResponse.NotFound;

            server.MetricsProvider.LogInventoryFetch(EXTENSION_NAME, ret, owner, itemID, false, DateTime.Now);
            return ret;
        }

        public BackendResponse TryFetchFolder(Uri owner, UUID agentID, UUID folderID, out InventoryFolder folder)
        {
            folder = null;
            UUID ownerID = CableBeachUtils.MessageToUUID(owner, agentID);
            BackendResponse ret;

            InventoryCollection collection;
            if (inventories.TryGetValue(ownerID, out collection) && collection.Folders.TryGetValue(folderID, out folder))
                ret = BackendResponse.Success;
            else
                ret = BackendResponse.NotFound;

            server.MetricsProvider.LogInventoryFetch(EXTENSION_NAME, ret, owner, folderID, true, DateTime.Now);
            return ret;
        }

        public BackendResponse TryFetchRootFolder(Uri owner, UUID agentID, out InventoryFolder folder)
        {
            folder = null;
            UUID ownerID = CableBeachUtils.MessageToUUID(owner, agentID);
            BackendResponse ret;

            InventoryCollection collection;
            if (inventories.TryGetValue(ownerID, out collection) && collection.Folders.TryGetValue(collection.RootFolderID, out folder))
                ret = BackendResponse.Success;
            else
                ret = BackendResponse.NotFound;

            server.MetricsProvider.LogInventoryFetch(EXTENSION_NAME, ret, owner, collection.RootFolderID, true, DateTime.Now);
            return ret;
        }

        public BackendResponse TryFetchFolderContents(Uri owner, UUID agentID, UUID folderID, out InventoryCollection contents)
        {
            contents = null;
            UUID ownerID = CableBeachUtils.MessageToUUID(owner, agentID);
            BackendResponse ret;

            InventoryCollection collection;
            InventoryFolder folder;

            if (inventories.TryGetValue(ownerID, out collection) && collection.Folders.TryGetValue(folderID, out folder))
            {
                contents = new InventoryCollection();
                contents.RootFolderID = folderID;
                contents.UserID = collection.UserID;
                contents.Folders = new Dictionary<UUID, InventoryFolder>();
                contents.Items = new Dictionary<UUID, InventoryItem>();
                contents.DefaultFolders = new Dictionary<string, InventoryFolder>();

                foreach (InventoryBase invBase in folder.Children.Values)
                {
                    if (invBase is InventoryItem)
                    {
                        InventoryItem invItem = invBase as InventoryItem;
                        contents.Items.Add(invItem.ID, invItem);
                    }
                    else
                    {
                        InventoryFolder invFolder = invBase as InventoryFolder;
                        contents.Folders.Add(invFolder.ID, invFolder);
                    }
                }

                ret = BackendResponse.Success;
            }
            else
            {
                ret = BackendResponse.NotFound;
            }

            server.MetricsProvider.LogInventoryFetchFolderContents(EXTENSION_NAME, ret, owner, folderID, DateTime.Now);
            return ret;
        }

        public BackendResponse TryFetchFolderList(Uri owner, UUID agentID, out List<InventoryFolder> folders)
        {
            folders = null;
            UUID ownerID = CableBeachUtils.MessageToUUID(owner, agentID);
            BackendResponse ret;

            InventoryCollection collection;
            if (inventories.TryGetValue(ownerID, out collection))
            {
                folders = new List<InventoryFolder>(collection.Folders.Values);
                return BackendResponse.Success;
            }
            else
            {
                ret = BackendResponse.NotFound;
            }

            server.MetricsProvider.LogInventoryFetchFolderList(EXTENSION_NAME, ret, owner, DateTime.Now);
            return ret;
        }

        public BackendResponse TryFetchFilesystem(Uri owner, UUID agentID, out InventoryCollection inventory)
        {
            inventory = null;
            UUID ownerID = CableBeachUtils.MessageToUUID(owner, agentID);
            BackendResponse ret;

            if (inventories.TryGetValue(ownerID, out inventory))
                ret = BackendResponse.Success;
            else
                ret = BackendResponse.NotFound;

            server.MetricsProvider.LogInventoryFetchInventory(EXTENSION_NAME, ret, owner, DateTime.Now);
            return ret;
        }

        public BackendResponse TryFetchActiveGestures(Uri owner, UUID agentID, out List<InventoryItem> gestures)
        {
            gestures = null;
            UUID ownerID = CableBeachUtils.MessageToUUID(owner, agentID);
            BackendResponse ret;

            if (activeGestures.TryGetValue(ownerID, out gestures))
                ret = BackendResponse.Success;
            else
                ret = BackendResponse.NotFound;

            server.MetricsProvider.LogInventoryFetchActiveGestures(EXTENSION_NAME, ret, owner, DateTime.Now);
            return ret;
        }

        public BackendResponse TryCreateItem(Uri owner, UUID agentID, InventoryItem item)
        {
            BackendResponse ret;
            UUID ownerID = CableBeachUtils.MessageToUUID(owner, agentID);
            InventoryCollection collection;

            item.Creator = item.Owner = ownerID;

            if (inventories.TryGetValue(ownerID, out collection))
            {
                // Delete this item first if it already exists
                InventoryItem oldItem;
                if (collection.Items.TryGetValue(item.ID, out oldItem))
                    TryDeleteItem(owner, agentID, item.ID);

                try
                {
                    lock (collection)
                    {
                        // Create the file
                        SaveItem(item);

                        // Add the item to the collection
                        collection.Items[item.ID] = item;
                    }

                    // Add the item to its parent folder
                    InventoryFolder parent;
                    if (collection.Folders.TryGetValue(item.ParentID, out parent))
                        lock (parent.Children) parent.Children.Add(item.ID, item);

                    // Add active gestures to our list
                    if (item.InvType == (int)InventoryType.Gesture && item.Flags == 1)
                    {
                        lock (activeGestures)
                            activeGestures[ownerID].Add(item);
                    }

                    ret = BackendResponse.Success;
                }
                catch (Exception ex)
                {
                    Logger.Error(ex);
                    ret = BackendResponse.Failure;
                }
            }
            else
            {
                return BackendResponse.NotFound;
            }

            server.MetricsProvider.LogInventoryCreate(EXTENSION_NAME, ret, owner, false, DateTime.Now);
            return ret;
        }

        public BackendResponse TryCreateFolder(Uri owner, UUID agentID, InventoryFolder folder)
        {
            BackendResponse ret;
            UUID ownerID = CableBeachUtils.MessageToUUID(owner, agentID);
            InventoryCollection collection;

            folder.Owner = ownerID;

            if (inventories.TryGetValue(ownerID, out collection))
            {
                // Delete this folder first if it already exists
                InventoryFolder oldFolder;
                if (collection.Folders.TryGetValue(folder.ID, out oldFolder))
                {
                    folder.Version = (ushort)(oldFolder.Version + 1);
                    TryDeleteFolder(owner, agentID, folder.ID);
                }
                else
                {
                    folder.Version = 1;
                }

                try
                {
                    // Sanity check
                    if (folder.Children == null)
                        folder.Children = new Dictionary<UUID, InventoryBase>();

                    lock (collection)
                    {
                        // Create the file
                        SaveFolder(folder);

                        // Add the folder to the collection
                        collection.Folders[folder.ID] = folder;

                        AssetType preferredType = (AssetType)folder.Type;
                        if (preferredType != AssetType.Unknown)
                        {
                            // Set this folder to the default folder for a content type if no other folder is already
                            // set for that content type
                            string contentType = CableBeachUtils.SLAssetTypeToContentType((short)preferredType);
                            if (!collection.DefaultFolders.ContainsKey(contentType))
                                collection.DefaultFolders[contentType] = folder;
                        }
                    }

                    // Add the folder to its parent folder
                    InventoryFolder parent;
                    if (collection.Folders.TryGetValue(folder.ParentID, out parent))
                        lock (parent.Children) parent.Children.Add(folder.ID, folder);

                    ret = BackendResponse.Success;
                }
                catch (Exception ex)
                {
                    Logger.Error(ex.Message);
                    ret = BackendResponse.Failure;
                }
            }
            else
            {
                ret = BackendResponse.NotFound;
            }

            server.MetricsProvider.LogInventoryCreate(EXTENSION_NAME, ret, owner, true, DateTime.Now);
            return ret;
        }

        public BackendResponse TryCreateFilesystem(Uri owner, UUID agentID, InventoryFolder rootFolder)
        {
            BackendResponse ret;
            UUID ownerID = CableBeachUtils.MessageToUUID(owner, agentID);

            lock (inventories)
            {
                if (!inventories.ContainsKey(ownerID))
                {
                    InventoryCollection collection = new InventoryCollection();
                    collection.RootFolderID = rootFolder.ID;
                    collection.UserID = rootFolder.Owner;
                    collection.Folders = new Dictionary<UUID, InventoryFolder>();
                    collection.Folders.Add(rootFolder.ID, rootFolder);
                    collection.Items = new Dictionary<UUID, InventoryItem>();
                    collection.DefaultFolders = new Dictionary<string, InventoryFolder>();

                    inventories.Add(ownerID, collection);

                    ret = BackendResponse.Success;
                }
                else
                {
                    ret = BackendResponse.Failure;
                }
            }

            if (ret == BackendResponse.Success)
            {
                string path = Path.Combine(DEFAULT_INVENTORY_DIR, rootFolder.Owner.ToString());
                try
                {
                    // Create the directory for this agent
                    Directory.CreateDirectory(path);

                    // Create an index.txt containing the UUID and URI for this agent
                    string[] index = new string[] { rootFolder.Owner.ToString(), owner.ToString() };
                    File.WriteAllLines(Path.Combine(path, "index.txt"), index);

                    // Create the root folder file
                    SaveFolder(rootFolder);
                }
                catch (Exception ex)
                {
                    Logger.Error(ex.Message);
                    ret = BackendResponse.Failure;
                }
            }

            server.MetricsProvider.LogInventoryCreateInventory(EXTENSION_NAME, ret, DateTime.Now);
            return ret;
        }

        public BackendResponse TryDeleteItem(Uri owner, UUID agentID, UUID itemID)
        {
            BackendResponse ret;
            UUID ownerID = CableBeachUtils.MessageToUUID(owner, agentID);

            InventoryCollection collection;
            InventoryItem item;
            if (inventories.TryGetValue(ownerID, out collection) && collection.Items.TryGetValue(itemID, out item))
            {
                // Remove the item from its parent folder
                InventoryFolder parent;
                if (collection.Folders.TryGetValue(item.ParentID, out parent))
                    lock (parent.Children) parent.Children.Remove(itemID);

                // Remove the item from the collection
                lock (collection) collection.Items.Remove(itemID);

                // Remove from the active gestures list if applicable
                if (item.InvType == (int)InventoryType.Gesture)
                {
                    lock (activeGestures)
                    {
                        for (int i = 0; i < activeGestures[ownerID].Count; i++)
                        {
                            if (activeGestures[ownerID][i].ID == itemID)
                            {
                                activeGestures[ownerID].RemoveAt(i);
                                break;
                            }
                        }
                    }
                }

                // Delete the file. We don't know exactly what the file name is, so search for it
                string path = PathFromUUID(ownerID);
                try
                {
                    string[] matches = Directory.GetFiles(path, String.Format("*{0}.item", itemID), SearchOption.TopDirectoryOnly);
                    foreach (string match in matches)
                    {
                        try { File.Delete(match); }
                        catch (Exception ex) { Logger.ErrorFormat("[SimpleInventory] Failed to delete item {0} from directory {1}: {2}", match, path, ex.Message); }
                    }
                }
                catch (Exception ex) { Logger.ErrorFormat("[SimpleInventory] Failed to delete item from directory {0}: {1}", path, ex.Message); }

                ret = BackendResponse.Success;
            }
            else
            {
                ret = BackendResponse.NotFound;
            }

            server.MetricsProvider.LogInventoryDelete(EXTENSION_NAME, ret, owner, itemID, false, DateTime.Now);
            return ret;
        }

        public BackendResponse TryDeleteFolder(Uri owner, UUID agentID, UUID folderID)
        {
            BackendResponse ret;
            UUID ownerID = CableBeachUtils.MessageToUUID(owner, agentID);

            InventoryCollection collection;
            InventoryFolder folder;
            if (inventories.TryGetValue(ownerID, out collection) && collection.Folders.TryGetValue(folderID, out folder))
            {
                // Remove the folder from its parent folder
                InventoryFolder parent;
                if (collection.Folders.TryGetValue(folder.ParentID, out parent))
                    lock (parent.Children) parent.Children.Remove(folderID);

                // Remove the folder from the collection
                lock (collection) collection.Items.Remove(folderID);

                // Delete the folder file. We don't know exactly what the file name is, so search for it
                string path = PathFromUUID(ownerID);
                try
                {
                    string[] matches = Directory.GetFiles(path, String.Format("*{0}.folder", folderID), SearchOption.TopDirectoryOnly);
                    foreach (string match in matches)
                    {
                        try { File.Delete(match); }
                        catch (Exception ex) { Logger.ErrorFormat("[SimpleInventory] Failed to delete folder file {0} from directory {1}: {2}", match, path, ex.Message); }
                    }
                }
                catch (Exception ex) { Logger.ErrorFormat("[SimpleInventory] Failed to delete folder from directory {0}: {1}", path, ex.Message); }

                ret = BackendResponse.Success;
            }
            else
            {
                ret = BackendResponse.NotFound;
            }

            server.MetricsProvider.LogInventoryDelete(EXTENSION_NAME, ret, owner, folderID, true, DateTime.Now);
            return ret;
        }

        public BackendResponse TryPurgeFolder(Uri owner, UUID agentID, UUID folderID)
        {
            BackendResponse ret;
            UUID ownerID = CableBeachUtils.MessageToUUID(owner, agentID);

            InventoryCollection collection;
            InventoryFolder folder;
            if (inventories.TryGetValue(ownerID, out collection) && collection.Folders.TryGetValue(folderID, out folder))
            {
                // Delete all of the folder children
                foreach (InventoryBase obj in new List<InventoryBase>(folder.Children.Values))
                {
                    if (obj is InventoryItem)
                    {
                        TryDeleteItem(owner, agentID, (obj as InventoryItem).ID);
                    }
                    else
                    {
                        InventoryFolder childFolder = obj as InventoryFolder;
                        TryPurgeFolder(owner, agentID, childFolder.ID);
                        TryDeleteFolder(owner, agentID, childFolder.ID);
                    }
                }

                ret = BackendResponse.Success;
            }
            else
            {
                ret = BackendResponse.NotFound;
            }

            server.MetricsProvider.LogInventoryPurgeFolder(EXTENSION_NAME, ret, owner, folderID, DateTime.Now);
            return ret;
        }

        public UUID GetDefaultAsset(Uri owner, UUID agentID, string contentType)
        {
            Logger.Warn("[SimpleInventory] Returning default assetID of UUID.Zero for " + contentType);
            return UUID.Zero;
        }

        public UUID GetDefaultParent(Uri owner, UUID agentID, string contentType)
        {
            InventoryFolder defaultFolder;
            InventoryCollection collection;
            UUID ownerID = CableBeachUtils.MessageToUUID(owner, agentID);

            if (inventories.TryGetValue(ownerID, out collection))
            {
                if (collection.DefaultFolders.TryGetValue(contentType, out defaultFolder))
                    return defaultFolder.ID;
                else
                    return collection.RootFolderID;
            }
            else
            {
                Logger.Warn("[SimpleInventory] GetDefaultParent called for an unknown inventory " + owner);
                return UUID.Zero;
            }
        }

        public UUID IdentityToUUID(Uri owner)
        {
            // FIXME: We should be storing a mapping from Uri (identity) to UUID (agentID) for every filesystem
            return CableBeachUtils.IdentityToUUID(owner);
        }

        #endregion Required Interfaces

        void SaveItem(InventoryItem item)
        {
            string filename = String.Format("{0}-{1}.item", SanitizeFilename(item.Name), item.ID);

            string path = Path.Combine(DEFAULT_INVENTORY_DIR, item.Owner.ToString());
            path = Path.Combine(path, filename);

            using (FileStream stream = new FileStream(path, FileMode.Create, FileAccess.Write))
            {
                itemSerializer.Serialize(stream, item);
                stream.Flush();
            }
        }

        void SaveFolder(InventoryFolder folder)
        {
            string filename = String.Format("{0}-{1}.folder", SanitizeFilename(folder.Name), folder.ID);

            string path = Path.Combine(DEFAULT_INVENTORY_DIR, folder.Owner.ToString());
            path = Path.Combine(path, filename);

            using (FileStream stream = new FileStream(path, FileMode.Create, FileAccess.Write))
            {
                folderSerializer.Serialize(stream, folder);
                stream.Flush();
            }
        }

        string SanitizeFilename(string filename)
        {
            string output = filename;

            if (output.Length > 64)
                output = output.Substring(0, 64);

            foreach (char i in Path.GetInvalidFileNameChars())
                output = output.Replace(i, '_');

            return output;
        }

        static string PathFromUUID(UUID ownerID)
        {
            return Path.Combine(DEFAULT_INVENTORY_DIR, ownerID.ToString());
        }

        void LoadFiles()
        {
            try
            {
                // Try to create the directory if it doesn't already exist
                if (!Directory.Exists(DEFAULT_INVENTORY_DIR))
                    Directory.CreateDirectory(DEFAULT_INVENTORY_DIR);

                string[] agentFolders = Directory.GetDirectories(DEFAULT_INVENTORY_DIR);

                for (int i = 0; i < agentFolders.Length; i++)
                {
                    string foldername = agentFolders[i];
                    string indexPath = Path.Combine(foldername, "index.txt");
                    UUID ownerID = UUID.Zero;
                    Uri owner = null;

                    try
                    {
                        string[] index = File.ReadAllLines(indexPath);
                        ownerID = UUID.Parse(index[0]);
                        owner = new Uri(index[1]);
                    }
                    catch (Exception ex)
                    {
                        Logger.WarnFormat("[SimpleInventory] Failed loading the index file {0}: {1}", indexPath, ex.Message);
                    }

                    if (ownerID != UUID.Zero && owner != null)
                    {
                        // Initialize the active gestures list for this agent
                        activeGestures.Add(ownerID, new List<InventoryItem>());

                        InventoryCollection collection = new InventoryCollection();
                        collection.UserID = ownerID;
                        collection.DefaultFolders = new Dictionary<string, InventoryFolder>();

                        // Load all of the folders for this agent
                        string[] folders = Directory.GetFiles(foldername, "*.folder", SearchOption.TopDirectoryOnly);
                        collection.Folders = new Dictionary<UUID,InventoryFolder>(folders.Length);

                        for (int j = 0; j < folders.Length; j++)
                        {
                            InventoryFolder invFolder = (InventoryFolder)folderSerializer.Deserialize(
                                new FileStream(folders[j], FileMode.Open, FileAccess.Read));
                            collection.Folders[invFolder.ID] = invFolder;

                            if (invFolder.ParentID == UUID.Zero)
                                collection.RootFolderID = invFolder.ID;
                        }

                        // Iterate over the folders collection, adding children to their parents
                        // and setting default folders
                        foreach (InventoryFolder invFolder in collection.Folders.Values)
                        {
                            InventoryFolder parent;
                            if (collection.Folders.TryGetValue(invFolder.ParentID, out parent))
                                parent.Children[invFolder.ID] = invFolder;

                            AssetType preferredType = (AssetType)invFolder.Type;
                            if (preferredType != AssetType.Unknown)
                                collection.DefaultFolders[CableBeachUtils.SLAssetTypeToContentType((int)preferredType)] = invFolder;
                        }

                        // Load all of the items for this agent
                        string[] files = Directory.GetFiles(foldername, "*.item", SearchOption.TopDirectoryOnly);
                        collection.Items = new Dictionary<UUID, InventoryItem>(files.Length);

                        for (int j = 0; j < files.Length; j++)
                        {
                            InventoryItem invItem = (InventoryItem)itemSerializer.Deserialize(
                                new FileStream(files[j], FileMode.Open, FileAccess.Read));
                            collection.Items[invItem.ID] = invItem;

                            // Add items to their parent folders
                            InventoryFolder parent;
                            if (collection.Folders.TryGetValue(invItem.ParentID, out parent))
                                parent.Children[invItem.ID] = invItem;

                            // Add active gestures to our list
                            if (invItem.InvType == (int)InventoryType.Gesture && invItem.Flags != 0)
                                activeGestures[ownerID].Add(invItem);
                        }

                        inventories.Add(ownerID, collection);
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.ErrorFormat("[SimpleInventory] Failed loading inventory from {0}: {1}", DEFAULT_INVENTORY_DIR, ex.Message);
            }
        }
    }
}